package speculoos.jndi.pool;

import java.lang.reflect.Constructor;
import java.util.EmptyStackException;
import java.util.Map;
import java.util.Stack;

import javax.naming.directory.DirContext;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import speculoos.jndi.JNDISource;
import speculoos.manager.MapperConfigurationException;
import speculoos.spi.MapperTechnicalException;
import speculoos.utils.TypeHelper;

/**
 * A JNDI Source that owns pool of connections.
 * 
 * This connection pool sets hard upper limits to the number of connections it
 * may handle, and put overhead threads in a wait queue.
 * 
 * @author jfhelie
 * @author nono
 * @version $Id$
 */
public class JNDIPooledSource extends JNDISource {

	private static final Log log = LogFactory.getLog(JNDIPooledSource.class);

	private Stack pool;

	private int maxPoolSize;

	private int minPoolSize;

	private long timeout = JNDIPooledSource.DEFAULT_TIMEOUT;

	/*
	 * used to collect statistics on pooling usage and performance
	 */
	private PoolingStats stats = new PoolingStats();

	/*
	 * access control to pool
	 */
	private Object lock;

	/*
	 * The connection factory to use
	 */
	private ConnectionFactory factory;

	/**
	 * Default maximume pool size
	 */
	public static final int DEFAULT_MAX_POOL = 20;

	/**
	 * Default minimum pool size
	 */
	public static final int DEFAULT_MIN_POOL = 10;

	/**
	 * Default timeout in milliseconds
	 */
	public static final long DEFAULT_TIMEOUT = 300;

	public static final String MAX_POOL_SIZE = "jndi.connection.pool.maxPoolSize";

	public static final String MIN_POOL_SIZE = "jndi.connection.pool.minPoolSize";

	public static final String TIMEOUT = "jndi.connection.pool.timeout";

	public static final String FACTORY = "jndi.connection.pool.factory";

	/**
	 * Create pooled source with given name;
	 * 
	 * @param name
	 */
	public JNDIPooledSource(String name) {
		super(name);
	}

	/**
	 * Create anonymous pooled source. This source is created with default name
	 * 'pooled'.
	 * 
	 */
	public JNDIPooledSource() {
		super("pooled");
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see speculoos.spi.Source#start(java.util.Map)
	 */
	public void start(Map params) throws MapperConfigurationException {
		if (log.isDebugEnabled())
			log.debug("[" + getName()
					+ "] starting pooled source with parameters " + params);
		if (log.isInfoEnabled())
			log.info("[" + getName() + "] starting pooled source");
		/* start super class */
		super.start(params);
		/* retrieve environment */
		Map env = getCurrent();
		String s = (String) env.get(JNDIPooledSource.MIN_POOL_SIZE);
		if (s == null)
			minPoolSize = JNDIPooledSource.DEFAULT_MIN_POOL;
		else
			try {
				minPoolSize = Integer.parseInt(s);
			} catch (NumberFormatException e) {
				throw new MapperConfigurationException(
						"Error in minimum size: " + s, e);
			}
		s = (String) env.get(JNDIPooledSource.MAX_POOL_SIZE);
		if (s == null)
			maxPoolSize = JNDIPooledSource.DEFAULT_MAX_POOL;
		else
			try {
				maxPoolSize = Integer.parseInt(s);
			} catch (NumberFormatException e) {
				throw new MapperConfigurationException(
						"Error in maximum size: " + s, e);
			}
		s = (String) env.get(JNDIPooledSource.TIMEOUT);
		if (s == null)
			timeout = JNDIPooledSource.DEFAULT_TIMEOUT;
		else
			try {
				timeout = Integer.parseInt(s);
			} catch (NumberFormatException e) {
				throw new MapperConfigurationException(
						"Error in timeout: " + s, e);
			}
		s = (String) env.get(JNDIPooledSource.FACTORY);
		if (s == null)
			this.factory = new JNDIConnectionFactory(env);
		else {
			try {
				Class cl = Class.forName(s);
				Constructor ctor = cl
						.getDeclaredConstructor(new Class[] { Map.class });
				this.factory = (ConnectionFactory) ctor
						.newInstance(new Object[] { env });
			} catch (NoSuchMethodException e) {
				throw new MapperConfigurationException(
						"No constructor for connection factory");
			} catch (ClassNotFoundException e) {
				throw new MapperConfigurationException(
						"Connection factory class " + s + " was not found");
			} catch (Exception e) {
				throw new MapperConfigurationException(
						"Error in constructing connection factory", e);
			}
		}

		try {
			validate();
		} catch (Exception e) {
			throw new MapperConfigurationException(
					"Got exception while configuring pool", e);
		}
		if (log.isDebugEnabled())
			log.debug("[" + getName()
					+ "] done starting pooled source with parameters : "
					+ new TypeHelper().toMap(this));
		if (log.isInfoEnabled())
			log.info("[" + getName() + "] done starting pooled source");
	}

	private void validate() throws Exception {
		if (minPoolSize <= 0)
			throw new IllegalArgumentException(
					"Minimum size must be greater than 0");
		if (maxPoolSize < minPoolSize)
			throw new IllegalArgumentException(
					"Maximum size must be greater than minimum size");
		if (timeout < 0)
			throw new IllegalArgumentException(
					"Timeout must be greater than or equal to 0");
		factory.setMaximumConnection(maxPoolSize);
		/* create pool */
		this.pool = new Stack();
		/* populate initial pool elements */
		for (int i = 0; i < minPoolSize; i++)
			pool.push(factory.connect());
		/* create pool lock */
		this.lock = new Object();
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see speculoos.spi.Source#stop()
	 */
	public void stop() {
		super.stop();
		this.closeAll();
		if (log.isInfoEnabled())
			log.info("[" + getName() + "] done stopping pooled source");
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see speculoos.jndi.JNDISource#connect()
	 */
	protected DirContext connect() throws MapperTechnicalException {
		DirContext ctx = null;
		while (ctx == null) {
			/*
			 * try to acquire a connection, either from the pool or by creating
			 * one if posssible. We keep the lock to update inuse.
			 */
			try {
				synchronized (pool) {
					ctx = (DirContext) pool.pop();
				}
			} catch (EmptyStackException e) {
				/* stack is empty. Create new connection */
				try {
					ctx = factory.connect();
				} catch (Exception e1) {
					throw new MapperTechnicalException(
							"Error in creating connection from factory", e1);
				}
			} catch (NullPointerException e) {
				/* pool is disabled */
				throw new MapperTechnicalException(
						"Pool closed while trying to connect");
			}
			/* done ? */
			if (ctx != null)
				break;

			/*
			 * we could not create a connection which means the maximum number
			 * of connections has been reached. we wait on this object until
			 * someone releases a connection
			 */
			stats.contention++;
			synchronized (this) {
				try {
					this.wait(timeout);
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
			}
		}
		stats.requests++;
		return ctx;
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see speculoos.jndi.pool.JNDIConnectionPool#closeAll()
	 */
	public void closeAll() {
		/* close all connections in pool */
		synchronized (pool) {
			while (true) {
				DirContext ctx = null;
				try {
					ctx = (DirContext) pool.pop();
					factory.disconnect(ctx);
				} catch (EmptyStackException e) {
					break;
				} catch (Exception e) {
					log.warn("Error in disconnecting " + ctx, e);
				}
			}
			/* clear */
			this.pool = null;
			this.factory = null;
		}
		/* wakeup sleeping threads */
		synchronized (this) {
			this.notifyAll();
		}

	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see speculoos.jndi.JNDISource#close(javax.naming.directory.DirContext)
	 */
	protected void close(DirContext ctx) {
		if (pool != null && pool.size() < maxPoolSize)
			synchronized (pool) {
				pool.push(ctx);
			}
		else
			try {
				factory.disconnect(ctx);
			} catch (Exception e) {
				log.warn("Error in disconnecting " + ctx, e);
			}
		/* wake up waiting threads */
		synchronized (this) {
			stats.releases++;
			this.notifyAll();
		}
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see speculoos.jndi.pool.JNDIConnectionPool#collectStatistics()
	 */
	public Map collectStatistics() {
		return new TypeHelper().toMap(stats);
	}

	/**
	 * @return Returns the factory.
	 */
	public ConnectionFactory getFactory() {
		return factory;
	}

	/**
	 * @return Returns the maxPoolSize.
	 */
	public int getMaxPoolSize() {
		return maxPoolSize;
	}

	/**
	 * @return Returns the minPoolSize.
	 */
	public int getMinPoolSize() {
		return minPoolSize;
	}

	/**
	 * @return Returns the timeout.
	 */
	public long getTimeout() {
		return timeout;
	}
}